﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using FluentPS.Attributes;
using FluentPS.Consts;
using FluentPS.Extensions;
using FluentPS.Services;
using Microsoft.Office.Project.Server.Library;

namespace FluentPS.Mapping
{
    internal class LookupValueUidsCollection
    {
        List<Guid> _ltStructUids = new List<Guid>();

        public LookupValueUidsCollection(Guid ltStructUid)
        {
            AddLtStructUid(ltStructUid);
        }

        public void AddLtStructUid(Guid ltStructUid)
        {
            if (!_ltStructUids.Contains(ltStructUid))
                _ltStructUids.Add(ltStructUid);
        }

        public List<Guid> LtStructUids
        {
            get { return _ltStructUids; }
        }
    }

    public class EntityDataMapper<T>
    {
        private readonly ILogService _logService;
        private ILookupTableService _lookupTablesService;

        public EntityDataMapper(ILogService logService, ILookupTableService lookupTablesService)
        {
            _logService = logService;
            _lookupTablesService = lookupTablesService;
        }

        public T MapDataRowToEntityNativeFields(DataRow row, T entity)
        {
            return MapDataRowToEntityNativeFields(row, entity, DataRowVersion.Default);
        }

        public T MapDataRowToEntityNativeFields(DataRow row, T entity, DataRowVersion dataRowVersion)
        {
            typeof(T).ForeachProperty(propertyInfo =>
            {
                var attr = typeof(T).GetPropertyCustomAttribute<PSNativeFieldAttribute>(propertyInfo);
                if (attr != null)
                {
                    string columnName = attr.ColumnName.Trim();
                    if (row.Table.Columns.Contains(columnName))
                    {
                        try
                        {
                            object val = RowDataAdapter.GetRowData(row, columnName, propertyInfo.PropertyType, dataRowVersion);
                            if (val != null || !propertyInfo.PropertyType.IsValueType)
                                propertyInfo.SetValue(entity, val, null);
                        }
                        catch (Exception ex)
                        {
                            _logService.Error("Native field mapping exception: " + ex.Message);
                            throw;
                        }
                    }
                }
            });

            return entity;
        }

        internal void SetPropertiesValues(T entity, Dictionary<Guid, object> customFieldsValues)
        {
            typeof(T).ForeachProperty(propertyInfo =>
            {
                var attr = typeof(T).GetPropertyCustomAttribute<PSCustomFieldAttribute>(propertyInfo);

                if (attr != null)
                {
                    Guid fieldId = attr.FieldUid;
                    if (customFieldsValues.ContainsKey(fieldId))
                    {
                        try
                        {
                            object val = customFieldsValues[fieldId];

                            if (attr is PSLookupFieldAttribute)
                            {
                                var lookupAttr = attr as PSLookupFieldAttribute;
                                if (val is LookupValueUidsCollection)
                                {
                                    var valueDto = val as LookupValueUidsCollection;
                                    if (valueDto.LtStructUids.Count > 0)
                                    {
                                        val = GetLookupData(lookupAttr, propertyInfo, valueDto);
                                    }
                                }
                            }

                            if (propertyInfo.PropertyType.Equals(typeof(int)) && val != null)
                            {
                                val = (int)val;
                            }

                            propertyInfo.SetValue(entity, val, null);
                        }
                        catch (Exception ex)
                        {
                            _logService.Error("Native field mapping exception: " + ex.Message);
                            throw;
                        }
                    }
                }
            });
        }

        internal void MapCustomFieldsToEntity(T entity, Guid entityUid, DataTable dtCustomFields, DataRowVersion version, ICustomFieldsMappingInfo cfMappingInfo)
        {
            var customFields = new Dictionary<Guid, object>();

            foreach (DataRow customFieldRow in dtCustomFields.Rows)
            {
                if (entityUid == RowDataAdapter.GetRowData<Guid>(customFieldRow, cfMappingInfo.EntityUidColumnName, version))
                {
                    try
                    {
                        var propId = RowDataAdapter.GetRowData<Guid>(
                            customFieldRow,
                            CustomFieldsColumnNames.MD_PROP_UID,
                            version);

                        object fieldValue;
                        Guid codeValue = RowDataAdapter.GetRowData<Guid>(
                            customFieldRow,
                            ProjectCustomFieldsColumnNames.CODE_VALUE,
                            version);
                        if (codeValue != Guid.Empty &&
                            RowDataAdapter.GetRowData<byte>(
                            customFieldRow,
                            ProjectCustomFieldsColumnNames.FIELD_TYPE_ENUM,
                            version) != (byte)PSDataType.GUID)
                        {
                            fieldValue = new LookupValueUidsCollection(codeValue);
                        }
                        else
                        {
                            fieldValue = GetCustomFieldRowValue(customFieldRow, version);
                        }

                        if (customFields.ContainsKey(propId) && (customFields[propId] is LookupValueUidsCollection))
                        {
                            ((LookupValueUidsCollection)customFields[propId]).AddLtStructUid(codeValue);
                        }
                        else
                        {
                            if (!customFields.ContainsKey(propId))
                                customFields.Add(propId, fieldValue);
                        }

                    }
                    catch (Exception ex)
                    {
                        _logService.Error("Custom fields mapping exception: " + ex.Message);
                        throw;
                    }
                }
            }

            // set loaded fields data to entity properties
            SetPropertiesValues(entity, customFields);
        }

        private static object GetCustomFieldRowValue(DataRow row, DataRowVersion version)
        {
            switch ((PSDataType)RowDataAdapter.GetRowData<byte>(row, CustomFieldsColumnNames.FIELD_TYPE_ENUM, version))
            {
                case PSDataType.GUID:
                    return RowDataAdapter.GetRowData<Guid>(row, CustomFieldsColumnNames.CODE_VALUE, version);
                case PSDataType.STRING:
                    return RowDataAdapter.GetRowData<string>(row, CustomFieldsColumnNames.TEXT_VALUE, version);
                case PSDataType.COST:
                case PSDataType.NUMBER:
                    return RowDataAdapter.GetRowData<decimal>(row, CustomFieldsColumnNames.NUM_VALUE, version);
                case PSDataType.DATE:
                    return RowDataAdapter.GetRowData<DateTime>(row, CustomFieldsColumnNames.DATE_VALUE, version);
                case PSDataType.YESNO:
                    return RowDataAdapter.GetRowData<bool>(row, CustomFieldsColumnNames.FLAG_VALUE, version);
                case PSDataType.DURATION:
                    return RowDataAdapter.GetRowData<int>(row, CustomFieldsColumnNames.DUR_VALUE, version);
                case PSDataType.INDICATOR:
                    return RowDataAdapter.GetRowData<int>(row, CustomFieldsColumnNames.INDICATOR_VALUE, version);
                default:
                    return null;
            }
        }


        private void SetCustomFieldRowValue(DataRow row, object data)
        {
            if (data == null)
                return;

            if (typeof(Guid).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.CODE_VALUE, data);
            else if (typeof(string).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.TEXT_VALUE, data);
            else if (typeof(decimal).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.NUM_VALUE, data);
            else if (typeof(DateTime).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.DATE_VALUE, data);
            else if (typeof(bool).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.FLAG_VALUE, data);
            else if (typeof(int).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.DUR_VALUE, data);
            else if (typeof(int).Equals(data.GetType()))
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.INDICATOR_VALUE, data);
            else
                throw new NotSupportedException(string.Format("Unsupported data type: {0}", data.GetType()));
        }


        internal TID GetEntityUid<TID>(T entity)
        {
            foreach (PropertyInfo propertyInfo in typeof(T).GetProperties())
            {
                if (propertyInfo.GetCustomAttribute<PSEntityUidFieldAttribute>() != null)
                {
                    return (TID)propertyInfo.GetValue(entity, null);
                }
            }

            throw new ArgumentException(string.Format("Unable to find ID property for class {0}", typeof(T)));
        }

        internal void MapEntityNativeFieldsToDataRow(T entity, DataRow row)
        {
            typeof(T).ForeachProperty(propertyInfo =>
            {
                var attr = typeof(T).GetPropertyCustomAttribute<PSNativeFieldAttribute>(propertyInfo);

                if (attr != null && !attr.IsReadOnly)
                {
                    string columnName = attr.ColumnName.Trim();
                    if (row.Table.Columns.Contains(columnName))
                    {
                        try
                        {
                            RowDataAdapter.SetRowData(row, columnName, propertyInfo.GetValue(entity, null));                            
                        }
                        catch (Exception ex)
                        {
                            _logService.Error("Custom fields mapping exception: " + ex.Message);
                            throw;
                        }
                    }
                }
            });
        }

        internal void MapCustomFieldsToDataSet(T entity, Guid entityUid, DataTable dtCustomFields, List<PropertyInfo> propertiesToUpdate, ICustomFieldsMappingInfo cfMappingInfo)
        {
            var mdPropsToUpdate = new Dictionary<Guid, PropertyInfo>();
            foreach (PropertyInfo propInfo in propertiesToUpdate)
            {
                var attr = propInfo.GetCustomAttribute<PSCustomFieldAttribute>();
                if (attr != null && !mdPropsToUpdate.ContainsKey(attr.FieldUid))
                {
                    mdPropsToUpdate.Add(attr.FieldUid, propInfo);
                }
            }

            foreach (DataRow customFieldRow in dtCustomFields.Rows)
            {
                if (entityUid == RowDataAdapter.GetRowData<Guid>(customFieldRow, cfMappingInfo.EntityUidColumnName))
                {
                    try
                    {
                        var propUid = RowDataAdapter.GetRowData<Guid>(customFieldRow, CustomFieldsColumnNames.MD_PROP_UID);
                        if (mdPropsToUpdate.ContainsKey(propUid))
                        {
                            object fieldValue = mdPropsToUpdate[propUid].GetValue(entity, null);
                            SetCustomFieldRowValue(customFieldRow, fieldValue);
                            mdPropsToUpdate.Remove(propUid);
                        }

                    }
                    catch (Exception ex)
                    {
                        _logService.Error("Custom fields mapping exception: " + ex.Message);
                        throw;
                    }
                }
            }

            // add new values
            foreach (var mdPropData in mdPropsToUpdate)
            {
                var propUid = mdPropData.Key;
                object fieldValue = mdPropsToUpdate[propUid].GetValue(entity, null);

                if (fieldValue == null)
                    continue;
                var row = dtCustomFields.NewRow();
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.MD_PROP_UID, propUid);
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.CUSTOM_FIELD_UID, Guid.NewGuid());
                RowDataAdapter.SetRowData(row, ProjectCustomFieldsColumnNames.PROJ_UID, entityUid);
                RowDataAdapter.SetRowData(row, CustomFieldsColumnNames.FIELD_TYPE_ENUM, (byte)PSDataTypesHelper.GetFieldType(fieldValue.GetType()));
                SetCustomFieldRowValue(row, fieldValue);

                dtCustomFields.Rows.Add(row);
            }
        }

        private object GetLookupData(PSLookupFieldAttribute attr, PropertyInfo propertyInfo, LookupValueUidsCollection lookupValueDto)
        {
            if (!propertyInfo.PropertyType.IsEnumerableGeneric())
            {
                return _lookupTablesService.GetLookupTableValue(
                    attr.LookupTableUid,
                    lookupValueDto.LtStructUids[0],
                    propertyInfo.PropertyType);
            }

            var dataType = propertyInfo.PropertyType.GetGenericArguments()[0];
            return _lookupTablesService.GetLookupTableValues(
                attr.LookupTableUid,
                lookupValueDto.LtStructUids,
                dataType);
        }

    }
}
